原创 | 驱动病毒那些事(二)—— 回调
点击上方蓝字 关注我吧
文章有些长,请大家耐心阅读。
注册进程回调的函数为函数为PsSetCreateProcessNotifyRoutine,看一下该函数的定义
在驱动病毒中,注册进程回调的行为非常常见,比如病毒注册进程回调,当打开浏览器时,修改ProcessParameters中的cmdline参数实现劫持主页的目的。
如果想要摘除其注册的进程回调,仅需找到NotifyRoutine地址,然后构造
PsSetCreateProcessNotifyRoutine(callbackaddr,TRUE)即可摘除进程回调。当然病毒也可以通过此方式摘除杀软的进程回调来保护自身模块。
下面我们来看一下PsSetCreateProcessNotifyRoutine具体的执行流程,我将结合WRK源码和windbg反汇编的结果来为大家展示函数执行全过程(以win7 x64为例,xp执行流程稍有不同)。
我们用dq命令,查看PspCreateProcessNotifyRoutine内容,惊奇的发现了8个地址,此时打开Pchunter,在主机上存在8个进程回调,此时我天真的以为PspCreateProcessNotifyRoutine中存的地址就是进程回调函数的地址,但是和Pchunter中的地址进行,发现并不一样,苦恼。
但是我坚信这两种之前一定存在着某种关联。
PsSetCreateProcessNotifyRoutine函数首先调用了ExReferenceCallBackBlock函数,而传入该函数的参数正是我们之前看到的PspCreateProcessNotifyRoutine, PspCreateProcessNotifyRoutine指向存在回调结构的数组。
而且我们观察到该函数的返回值类型为EX_CALLBACK_ROUTINE_BLOCK,一个结构体。
查看该结构体细节:
typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;
我们进入ExReferenceCallBackBlock函数观察细节。
先看一下函数传入的参数和返回值,再结合函数描述,这个函数的作用应该是取得回调结构的调用。
PspCreateProcessNotifyRoutine数组中保存的是 PEX_CALLBACK 类型的结构地址数组。
看一下EX_CALLBACK结构体,结构中只有一个成员,就是 RoutineBlock ,它是一个 EX_FAST_REF 结构。
typedef struct _EX_CALLBACK {
EX_FAST_REF RoutineBlock;
} EX_CALLBACK, *PEX_CALLBACK;
查看结构体成员,该结构体有两个成员,引用计数变量和Value指针。重要的是value指针,它指向的正是之前提到过的EX_CALLBACK_ROUTINE_BLOCK结构。
typedef struct _EX_FAST_REF {
union {
PVOID Object;
#if defined (_WIN64)
ULONG_PTR RefCnt : 4;
#else
ULONG_PTR RefCnt : 3;
#endif
ULONG_PTR Value;
};
} EX_FAST_REF, *PEX_FAST_REF;
ExFastRefGetObject 将传进来的EX_FAST_REF指针进行了与运算就得到 PEX_CALLBACK_ROUTINE_BLOCK 结构地址。
现在我们基本理清了操作系统获取回调函数地址的整个流程,下面进行验证。之前我们已经获取了PspCreateProcessNotifyRoutine地址。我们拿ffff8a00103a91f进行举例。
用ffff8a00103a91f&~15即可得到EX_CALLBACK_ROUTINE_BLOCK结构体地址。
typedef struct _EX_CALLBACK_ROUTINE_BLOCK {
EX_RUNDOWN_REF RundownProtect;
PEX_CALLBACK_FUNCTION Function;
PVOID Context;
} EX_CALLBACK_ROUTINE_BLOCK, *PEX_CALLBACK_ROUTINE_BLOCK;
我们来梳理一下思路。PsSetCreateProcessNotifyRoutine函数调用PspSetCreateProcessNotifyRoutine函数,在PspSetCreateProcessNotifyRoutine函数中可以获取PspCreateProcessNotifyRoutine,在PspCreateProcessNotifyRoutine中 保存的是指针数组。
系统将数组中的指针传递给 ExFastRefGetObject,就得到了PEX_CALLBACK_ROUTINE_BLOCK 结构地址,这个结构中保存着我们想要的回调例程地址;
获取到callback地址后,如果传进来的参数为FALSE,即注册一个进程回调。首先会调用ExAllocateCallBack申请一个块内存,内容为null。
接着调用ExCompareExchangeCallBack,在数组PspCreateProcessNotifyRoutine找一个空位存放callback,最后将全局计数变量PspCreateProcessNotifyRoutineCount加1。
同理,我们如果想要摘除进程回调,只需找到callback地址,调用PsSetCreateProcessNotifyRoutine(callback,TRUE)即可。
在调用PsSetCreateProcessNotifyRoutine函数注册进程回调时,会传入两个参数
PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine和 BOOLEAN Remove。
而NotifyRoutine 的参数类型为VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE)( IN HANDLE ParentId,IN HANDLE ProcessId,IN BOOLEAN Create);
也就是说当有新的进程被创建时,会把父进程的ID,和子进程(被创建的进程)ID传给回调函数,我们可以同时获取子进程ID和父进程ID。
我们可以通过PsLookupProcessByProcessId函数根据PID获取EPROCESS结构体,进而获取有关被创建进程更多的信息。
详情可以参考https://blog.csdn.net/cssxn/article/details/101352152。
线程回调和进程回调的过程大同小异,在此不再详述。
自然注册表也是驱动病毒必争之地,病毒可以通过注册一个注册表回调来对抗杀软对自己注册表项的清除操作等等。
首先看一下函数声明:
NTSTATUS CmRegisterCallback(
_In_ PEX_CALLBACK_FUNCTION Function,
_In_opt_ PVOID Context,
_Out_ PLARGE_INTEGER Cookie
);
参数
Function[in]
指向RegistryCallback例程的指针。
Context[in]
配置管理器将作为CallbackContext参数传递给RegistryCallback例程的驱动程序定义的值。
Cookie [out]
指向LARGE_INTEGER变量的指针,该变量接收标识回调例程的值。当您注销回调例程时,将此值作为Cookie参数传递给CmUnRegisterCallback。
返回值
成功,则返回 STATUS_SUCCESS;否则,返回其它失败错误码 NTSTATUS。
其中我们需要重点关注的是Function即我们回调函数的地址,以及回调函数的句柄cookie。其中cookie是CmUnRegisterCallback 函数(删除注册表回调函数)唯一的的参数。
接着我们看一下回调函数原型。
EX_CALLBACK_FUNCTION RegistryCallback;
NTSTATUS RegistryCallback(
_In_ PVOID CallbackContext,
_In_opt_ PVOID Argument1,
_In_opt_ PVOID Argument2
)
参数
CallbackContext [in]
当注册该RegistryCallback例程时,驱动程序作为Context参数传递给CmRegisterCallback或CmRegisterCallbackEx的值。
Argument1 [in]
一个REG_NOTIFY_CLASS类型的值,用于标识正在执行的注册表操作的类型,以及是否在执行注册表操作之前或之后调用RegistryCallback例程。
Argument2 [in]
指向包含特定于注册表操作类型的信息的结构的指针。结构类型取决于Argument1的REG_NOTIFY_CLASS类型值,如下表所示。有关哪些REG_NOTIFY_CLASS类型的值可用于哪些操作系统版本的信息,请参阅REG_NOTIFY_CLASS。
返回值
成功,则返回 STATUS_SUCCESS;否则,返回其它失败错误码 NTSTATUS。
我们需要重点关注的是Argument1和Argument2。Argument1参数是注册表的操作类型, Argument2 参数作用是获取操作类型对应的结构体数据Object。从结构体数据中,我们可以获取注册表路径对象,调用ObQueryNameString函数根据路径对象获取字符串表示的路径。以此判断是否要拒绝操作的注册表路径,若是,则返回 STATUS_ACCESS_DENIED 拒绝操作,即可实现监控注册表的目的。
详情可以查看https://www.cnblogs.com/csnd/p/12062016.html
本节在分析注册表回调的时候就不再从源码层次去进行分析了,大体流程和进程回调相似,大家可以自行阅读WRK源码。咱们的重点是如何去摘除注册表回调。
注册表回调在XP系统上以数组的形式存储,从Windows 2003开始变成了链表结构,这个链表的头称为 CallbackListHead, 可在 CmUnRegisterCallback 中找到:
UINT_PTR GetCallbackListHeadAddress()
{
UINT_PTR CallbackListHead = 0;
UINT_PTR CmUnRegisterCallback = 0;
GetNtosExportVariableAddress(L"CmUnRegisterCallback", (PVOID*)&CmUnRegisterCallback);
DbgPrint("%p\r\n", CmUnRegisterCallback);
if (CmUnRegisterCallback)
{
PUINT8StartSearchAddress = (PUINT8)CmUnRegisterCallback;
PUINT8EndSearchAddress = StartSearchAddress + 0x500;
PUINT8i = NULL, j = NULL;
UINT8 v1 = 0, v2 = 0, v3 = 0;
INT32 iOffset = 0;
for (i = StartSearchAddress; i < EndSearchAddress; i++)
{
#ifdef _WIN64
if (MmIsAddressValid(i) && MmIsAddressValid(i + 1) && MmIsAddressValid(i + 2))
{
v1 = *i;
v2 = *(i + 1);
v3 = *(i + 2);
if (v1 == 0x48 && v2 == 0x8d && v3 == 0x0d)// 硬编码 lea rcx
{
j = i - 5;
if (MmIsAddressValid(j) && MmIsAddressValid(j + 1) && MmIsAddressValid(j + 2))
{
v1 = *j;
v2 = *(j + 1);
v3 = *(j + 2);
if (v1 == 0x48 && v2 == 0x8d && v3 == 0x54)// 硬编码 lea rdx
{
RtlCopyMemory(&iOffset, i + 3, 4);
CallbackListHead = (UINT_PTR)(iOffset + (UINT64)i + 7);
break;
}
}
}
}
#else
if (MmIsAddressValid(i) && MmIsAddressValid(i + 5) && MmIsAddressValid(i + 6))
{
v1 = *i;
v2 = *(i + 5);
v3 = *(i + 6);
if (v1 == 0xbf && v2 == 0x8b && v3 == 0xc7)// mov edi ...
{
RtlCopyMemory(&CallbackListHead, i + 1, 4);
break;
}
}
#endif // _WIN64
}
}
return CallbackListHead;
}
这样我们就可以获取到CallbackListHead地址。接下来就是枚举链表了,注册表回调是一个“结构体链表”, 类似于 EPROCESS,它的定义如下:
typedef struct _CM_NOTIFY_ENTRY
{
LIST_ENTRY ListEntryHead;
ULONG UnKnown1;
ULONG UnKnown2;
LARGE_INTEGER Cookie;
ULONG64 Context;
ULONG64 Function;
}CM_NOTIFY_ENTRY, *PCM_NOTIFY_ENTRY;
另一种是找到function地址,直接在目标回调地址上写一个 RET, 使其不执行任何代码就返回。
当然还有一种更加暴力的方法,e CmpCallBackCount 0清空计数变量。我们在这里演示第一种方法。
大家如果对第二种方法感兴趣,可以参考http://www.dbgpro.com/archives/4948.html 。
ULONG DelCmpCallback(ULONG64* pPspLINotifyRoutine)
{
PVOID* h;
LARGE_INTEGERcookie;
NTSTATUS Status;
ULONGsum = 0;
ULONG64dwNotifyItemAddr;
ULONG64* pNotifyFun;
ULONG64* baseNotifyAddr;
ULONG64dwNotifyFun;
LARGE_INTEGERcmpCookie;
PLIST_ENTRYnotifyList;
PCM_NOTIFY_ENTRYnotify;
dwNotifyItemAddr = *pPspLINotifyRoutine;
notifyList = (LIST_ENTRY*)dwNotifyItemAddr;
while (!IsListEmpty(notifyList))
{
notify = (CM_NOTIFY_ENTRY*)notifyList;
if (MmIsAddressValid(notify))
{
if (MmIsAddressValid((PVOID)(notify->Function)) && notify->Function > 0x8000000000000000)
{
cookie = notify->Cookie;
sum++;
}
}
notifyList = notifyList->Flink;
Status = CmUnRegisterCallback(cookie);
if (NT_SUCCESS(Status))
{
DbgPrint("删除[CmCallback]Function=%p\t回调", (PVOID)(notify->Function));
}
}
return sum;
}
运行效果:
派遣例程。
对于驱动病毒来说,它们乐意在此阶段进行清理痕迹以及善后工作,进行攻击的收尾。
NTSTATUS IoRegisterShutdownNotification(
IN PDEVICE_OBJECT DeviceObject
)
注册关机回调函数的参数只有一个需要绑定的设备对象。
源代码分析
然后调用IopInterlockedInsertHeadList函数将结构体插入到NotifyShutdown队列之中。
我们查看SHUTDOWN_PACKET为何方神圣。
typedef struct _SHUTDOWN_PACKET {
LIST_ENTRY ListEntry;
PDEVICE_OBJECT DeviceObject;
} SHUTDOWN_PACKET, *PSHUTDOWN_PACKET;
一个ListEntry双向链表,还有一个DeviceObject,设备对象。
Amazing!!!
重大发现,这就说明我们可以通过硬编码来获取IopNotifyShutdownQueueHead地址,进而获取设备对象,通过设备对象又可以回溯找到驱动对象,有了驱动对象岂不是可以为所欲为之为所欲为,嘿嘿嘿。
关机回调摘除的思路也很简单,只需将指定回调从链表中移除即可。
下面来看具体的代码实现:
//获取IopNotifyShutdownQueueHead
NTSTATUS GetIopNotifyShutdownQueueHead()
{
ULONG_PTR i = 0;
ULONG OffsetAddr = 0;
LONG OffsetAddr64 = 0;
UNICODE_STRING strFunName;
RtlInitUnicodeString(&strFunName, L"IoRegisterShutdownNotification");
pIoRegisterShutdownNotification = MmGetSystemRoutineAddress(&strFunName);
if (pIoRegisterShutdownNotification == NULL)return STATUS_UNSUCCESSFUL;
DbgPrint("开始寻找链表head");
//8056ab93 8bd7 mov edx, edi
//8056ab95 b9e0285580 mov ecx, offset nt!IopNotifyShutdownQueueHead(805528e0)
for (i = pIoRegisterShutdownNotification; i < pIoRegisterShutdownNotification + 0xff; i++)
{
if (*(PUCHAR)i == 0x8b && *(PUCHAR)(i + 1) == 0xd7 && *(PUCHAR)(i + 2) == 0xb9)
{
RtlCopyMemory(&OffsetAddr, (PUCHAR)(i + 3), sizeof(ULONG_PTR));
break;
}
}
if (OffsetAddr && MmIsAddressValid(OffsetAddr))
{
NotifyRoutine = (PLIST_ENTRY)OffsetAddr;
//DbgPrint("函数获取成功");
return STATUS_SUCCESS;
}
}
//枚举移除IoRegisterShutdownNotification
NTSTATUS EnumRemoveShutdownNotification(void)
{
//定义变量
PLIST_ENTRY entry = NULL;
PSHUTDOWN_PACKET shutdown = NULL;
PDEVICE_OBJECT DevObj = NULL;
PDRIVER_OBJECT DrvObj = NULL;
ULONG Dispatch = 0;
ULONGsum = 0;
//获取IopNotifyShutdownQueueHead
if (IopNotifyShutdownQueueHead == NULL)
{
IopNotifyShutdownQueueHead = NotifyRoutine;
//DbgPrint("获取回调地址成功,%p\t", IopNotifyShutdownQueueHead);
if (IopNotifyShutdownQueueHead == NULL)return NULL;
}
for (entry = IopNotifyShutdownQueueHead->Flink; entry != IopNotifyShutdownQueueHead; entry = entry->Flink)
{
DevObj = (PDEVICE_OBJECT)(*(ULONG*)((ULONG)entry + sizeof(LIST_ENTRY)));
//DbgPrint("%p\t", DevObj);
DrvObj = DevObj->DriverObject;
Dispatch = (ULONG)(DrvObj->MajorFunction[IRP_MJ_SHUTDOWN]);
if (NULL != wcsstr(DrvObj->DriverName.Buffer, str.Buffer)) {
RemoveEntryList(entry);
//(ULONG)(DrvObj->MajorFunction[IRP_MJ_SHUTDOWN]) = NULL;
DbgPrint("删除%wZ\t关机回调成功", &DrvObj->DriverName);
}
else {
DbgPrint("[shutdown]0x%X\t\t%wZ\t\t", Dispatch, &DrvObj->DriverName);
sum++;
}
}
DbgPrint("目前主机共有关机回调函数:%d\t", sum);
return STATUS_SUCCESS;
}
在读取从应用层传入的字符串时,忽略了从应用程序传入的字符串为ANSI,就直接赋值给了UNICODE_STRING,结果蓝了一下午,大意了。
运行效果:
2.https://www.write-bug.com/article/2167.html
3.http://www.dbgpro.com/archives/4948.html
4.http://www.doc88.com/p-1187544953268.html
5.http://www.voidcn.com/article/p-qlfudnjc-ty.html